package com.example.uploadattachment.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.example.uploadattachment.exception.MyException;
import com.example.uploadattachment.model.FileEntity;
import com.example.uploadattachment.model.ReceiveFileEntity;
import com.example.uploadattachment.service.FileService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.SftpException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;

/**
 * 文件服务实现类
 *
 * @author 何逸诚
 * @date 2023/12/6 15:40
 */
@Slf4j
@Service
public class FileServiceImpl implements FileService {
    // 固定字段
    private static final String CONTENT = ".content_object_oss_aliyun_ALIBABA-1";
    private static final String METADATA = ".metadata_object_oss_aliyun_ALIBABA";
    // private static final String PREFIX = "/usr/local/oss-emulator-1.0.0/store";
    @Value("${sftp.path.prefix}")
    private String PREFIX;
    @Value("${sftp.path.bucket_name}")
    private String BUCKET_NAME;
    @Value("${sftp.path.tenant}")
    private String TENANT;

    @Autowired
    private GenericObjectPool<ChannelSftp> sftpPool;

    @Override
    public ReceiveFileEntity uploadFile(ReceiveFileEntity receiveFileEntity, boolean ifSftp) throws SftpException, IOException {
        System.out.println("==========================================================");

        log.info("入参 receiveFileEntity = {}", receiveFileEntity);
        String fileUrls = receiveFileEntity.getFileUrls();
        if (fileUrls == null || fileUrls.isEmpty()) {
            log.error("文件不存在");
            throw new MyException("文件不存在，请联系管理员");
        }

        // 将 JSON 字符串转化为对象
        ObjectMapper objectMapper;
        List<FileEntity> fileEntityList;
        try {
            objectMapper = new ObjectMapper();
            fileEntityList = objectMapper.readValue(fileUrls, new TypeReference<List<FileEntity>>() {
            });
        } catch (JsonProcessingException e) {
            log.error("JSON转化为文件对象失败");
            throw new MyException("JSON转化为文件对象失败，请联系管理员");
        }

        // 循环处理文件URL
        StringBuilder stringBuilder = new StringBuilder();
        for (FileEntity myFileEntity : fileEntityList) {
            log.info("文件实体 myFileEntity = {}", myFileEntity);
            FileEntity newFileEntity;
            if (ifSftp) {
                // 处理文件URL并上传到SFTP服务器
                newFileEntity = handFileUrlToSftp(myFileEntity);
            } else {
                // 处理文件URL并上传到本地
                newFileEntity = handFileUrlToLocal(myFileEntity);
            }
            log.info("构造新的文件实体 newFileEntity = {}", newFileEntity);

            // 将对象转换为 JSON 字符串
            String newJsonString;
            try {
                newJsonString = objectMapper.writeValueAsString(newFileEntity);
            } catch (JsonProcessingException e) {
                log.error("文件对象转化为JSON失败");
                throw new MyException("文件对象转化为JSON失败，请联系管理员");
            }
            log.info("构造新的JSON字符串 newJsonString = {}", newJsonString);

            if (!"".equals(stringBuilder.toString())) {
                stringBuilder.append(",").append(newJsonString);
            } else {
                stringBuilder.append(newJsonString);
            }
        }

        // 构造新URL
        ReceiveFileEntity newReceiveFileEntity = new ReceiveFileEntity();
        newReceiveFileEntity.setFileUrls("[" + stringBuilder + "]");
        log.info("构造新URL实体 newReceiveFileEntity = {}", newReceiveFileEntity);
        return newReceiveFileEntity;
    }

    /**
     * 处理文件URL并上传到SFTP服务器
     *
     * @param myFile
     * @return
     * @throws IOException
     */
    private FileEntity handFileUrlToSftp(FileEntity myFile) throws IOException {
        URL url = new URL(myFile.getUrl());

        ChannelSftp channelSftp = null;
        try (InputStream inputStream = url.openStream()) {

//            ChannelSftp channelSftp = sftpConnectChannel.channelSftp();
            channelSftp = sftpPool.borrowObject();
            // 文件类型
            String[] myFileSplit = myFile.getType().split("/");
            String newType = "att";
            if (myFileSplit[0].startsWith("image")) {
                newType = "img";
            }

            // 文件名后缀
            String[] myFileNameSplit = myFile.getFilename().split("\\.");
            String suffixName = "";
            if (myFileNameSplit.length > 1) {
                suffixName = "." + myFileNameSplit[1];
            }

            // 生成版本4的UUID，并将其转换为字符串
            String uuidString = UUID.randomUUID().toString();
            // 构造新的文件名称
            String newFilename = uuidString + suffixName;

            // 文件名前三个字符
            String prefixName = newFilename.substring(0, 3);

            // 根据时间戳文本获取yyyyMMdd格式日期文本
            String formattedDate = this.getFormattedDate(myFile.getDate());

            // http://crmtest.millisondc.com:7000/apaas-storage/302/img/20231121/1112971/30233df0-3044-44e2-9aea-39f3c894ea9d.png
            // 创建目录（包括缺失的父目录）
            String object = prefixName + "/" + newType + "/" + formattedDate + "/" + TENANT + "/" + newFilename;
            String saveDir = PREFIX + "/" + BUCKET_NAME + "/" + object;
            log.info("文件保存路径 saveDir = {}", saveDir);

            // 如果该路径不存在则直接创建新的目录，并切换到该目录 - 分割路径，逐级创建目录
            String[] dirs = saveDir.split("/");
            StringBuilder currentDir = new StringBuilder();
            for (String dir : dirs) {
                if (!dir.isEmpty()) {
                    currentDir.append("/").append(dir);
                    log.debug(currentDir.toString());
                    try {
                        channelSftp.cd(currentDir.toString());
                    } catch (Exception e1) {
                        try {
                            channelSftp.mkdir(currentDir.toString());
                            channelSftp.cd(currentDir.toString());
                        } catch (SftpException e) {
                            log.error("创建目录失败 = {}", e.getMessage());
                            throw new MyException("创建目录失败，请联系管理员");
                        }
                    }
                }
            }
            // 保存源文件
            channelSftp.put(inputStream, CONTENT);
            // 获取文件md5
            String md5 = DigestUtil.md5Hex(inputStream);

            // 构造文本
            String metadataText = "---\n" +
                    ":bucket: " + BUCKET_NAME + "\n" +
                    ":object: " + object + "\n" +
                    ":acl: default\n" +
                    ":creation_date: '" + this.generateISO8601Date() + "'\n" +
                    ":modified_date: '" + this.generateISO8601Date() + "'\n" +
                    ":content_type: " + myFile.getType() + "\n" +
                    ":content_disposition: filename=\"" + myFile.getFilename() + "\n" +
                    ":content_encoding: \n" +
                    ":size: " + myFile.getFilesize() + "\n" +
                    ":part_size: 0\n" +
                    ":md5: " + md5 + "\n" +
                    ":oss_metadata:\n" +
                    "  user-agent: aliyun-sdk-js/Browser version\n" +
                    "  security-token: test\n" +
                    ":custom_metadata: {}";
            // 将文本转化为流
            InputStream metadataTextInputStream = new ByteArrayInputStream(metadataText.getBytes(StandardCharsets.UTF_8));
            // 保存meta文件
            channelSftp.put(metadataTextInputStream, METADATA);

            // 输出新的FileEntity
            FileEntity newFileEntity = new FileEntity();
            BeanUtils.copyProperties(myFile, newFileEntity, "url");
            newFileEntity.setUrl(newFilename);
            return newFileEntity;
        } catch (Exception e) {
            log.error("连接SFTP失败 = {}", e.getMessage());
            throw new RuntimeException(e);
        } finally {
            // 归还通道实例
            if (channelSftp != null) {
                sftpPool.returnObject(channelSftp);
            }
        }
    }

    /**
     * 处理文件URL并上传到本地
     *
     * @param myFile
     * @return
     * @throws IOException
     */
    private FileEntity handFileUrlToLocal(FileEntity myFile) throws IOException {
        URL url = new URL(myFile.getUrl());
        try (InputStream inputStream = url.openStream()) {
            // 文件类型
            String[] myFileSplit = myFile.getType().split("/");
            String newType = "att";
            if (myFileSplit[0].startsWith("image")) {
                newType = "img";
            }

            // 文件名后缀
            String[] myFileNameSplit = myFile.getFilename().split("\\.");
            String suffixName = myFileNameSplit[1];

            // 生成版本4的UUID，并将其转换为字符串
            String uuidString = UUID.randomUUID().toString();
            // 构造新的文件名称
            String newFilename = uuidString + "." + suffixName;

            // 文件名前三个字符
            String prefixName = newFilename.substring(0, 3);

            // 根据时间戳文本获取yyyyMMdd格式日期文本
            String formattedDate = this.getFormattedDate(myFile.getDate());

            // http://crmtest.millisondc.com:7000/apaas-storage/302/img/20231121/1112971/30233df0-3044-44e2-9aea-39f3c894ea9d.png
            // 创建目录（包括缺失的父目录）
            String object = prefixName + File.separator
                    + newType + File.separator
                    + formattedDate + File.separator
                    + TENANT + File.separator
                    + newFilename;
            String saveDir = PREFIX + File.separator
                    + BUCKET_NAME + File.separator
                    + object;
            log.info("文件保存路径 saveDir = {}", saveDir);
            try {
                Files.createDirectories(Paths.get(saveDir));
            } catch (IOException e) {
                log.error("创建目录失败：{}", saveDir);
                throw new MyException("创建目录失败: " + saveDir);
            }

            // 保存源文件，并且返回文件MD5
            String md5 = this.saveContentFile(inputStream, saveDir);
            // 构造文本
            String metadataText = "---\n" +
                    ":bucket: " + BUCKET_NAME + "\n" +
                    ":object: " + object + "\n" +
                    ":acl: default\n" +
                    ":creation_date: '" + this.generateISO8601Date() + "'\n" +
                    ":modified_date: '" + this.generateISO8601Date() + "'\n" +
                    ":content_type: " + myFile.getType() + "\n" +
                    ":content_disposition: filename=\"" + myFile.getFilename() + "\n" +
                    ":content_encoding: \n" +
                    ":size: " + myFile.getFilesize() + "\n" +
                    ":part_size: 0\n" +
                    ":md5: " + md5 + "\n" +
                    ":oss_metadata:\n" +
                    "  user-agent: aliyun-sdk-js/Browser version\n" +
                    "  security-token: test\n" +
                    ":custom_metadata: {}";
            // 将文本保存为文件
            this.saveMetadataFile(metadataText, saveDir);

            // 输出新的FileEntity
            FileEntity newFileEntity = new FileEntity();
            BeanUtils.copyProperties(myFile, newFileEntity, "url");
            newFileEntity.setUrl(newFilename);
            return newFileEntity;
        }
    }

    /**
     * 保存源文件
     *
     * @param inputStream
     * @param saveDir
     * @return 文件MD5
     */
    private String saveContentFile(InputStream inputStream, String saveDir) {
        // 下载后的路径和文件名
        Path filePath = Paths.get(saveDir, CONTENT);
        // 下载文件并保存到指定目录
        try (FileOutputStream outputStream = new FileOutputStream(filePath.toFile())) {
            // 使用 Files.copy 复制输入流到输出流
            IOUtils.copy(inputStream, outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 文件保存成功后计算MD5
        try {
            return this.calculateFileMD5(filePath.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将文本保存为文件
     *
     * @param content
     * @param saveDir
     */
    private void saveMetadataFile(String content, String saveDir) {
        // 下载后的文件名和路径
        String filePath = Paths.get(saveDir, METADATA).toString();
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
            writer.write(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 文件保存成功后计算MD5
     *
     * @param filePath
     * @return
     */
    private String calculateFileMD5(String filePath) throws IOException, NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("MD5");
        try (FileInputStream fis = new FileInputStream(filePath);
             DigestInputStream dis = new DigestInputStream(fis, md)) {
            // 读取文件内容，同时计算MD5值
            byte[] buffer = new byte[4096];
            while (dis.read(buffer) != -1) {
                // 读取文件内容
            }
        }

        // 获取计算的MD5值
        byte[] mdBytes = md.digest();
        // 转换为十六进制字符串
        StringBuilder result = new StringBuilder();
        for (byte mdByte : mdBytes) {
            result.append(Integer.toString((mdByte & 0xff) + 0x100, 16).substring(1));
        }
        return result.toString();
    }

    /**
     * 通过输入流获得MD5
     *
     * @param inputStream
     * @return
     */
    public String getInputStreamMD5(InputStream inputStream) {
        // Use MD5 algorithm
        MessageDigest md5Digest = null;
        try {
            md5Digest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        // Get the checksum
        String checksum = null;
        try {
            checksum = getInputStreamChecksum(md5Digest, inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // see checksum
        // System.out.println(checksum);
        return checksum;
    }

    private static String getInputStreamChecksum(MessageDigest digest, InputStream inputStream) throws IOException {
        // Create byte array to read data in chunks
        byte[] byteArray = new byte[1024];
        int bytesCount = 0;

        //Read file data and update in message digest
        while ((bytesCount = inputStream.read(byteArray)) != -1) {
            digest.update(byteArray, 0, bytesCount);
        }

        // close the stream; We don't need it now.
        inputStream.close();

        // Get the hash's bytes
        byte[] bytes = digest.digest();

        // This bytes[] has bytes in decimal format;
        // Convert it to hexadecimal format
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
        }

        // return complete hash
        return sb.toString();
    }

    /**
     * 获取当前日期
     */
    private String generateISO8601Date() {
        return DateUtil.now();
    }

    /**
     * 根据时间戳文本获取yyyyMMdd格式日期文本
     *
     * @param timestamp
     */
    private String getFormattedDate(String timestamp) {
        // 将时间戳字符串转化为 Instant
        Instant instant = Instant.ofEpochMilli(Long.parseLong(timestamp));
        // 将 Instant 转化为 LocalDate
        LocalDate localDate = instant.atZone(ZoneId.systemDefault()).toLocalDate();
        // 使用 DateTimeFormatter 格式化为 "yyyyMMdd" 形式
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        return localDate.format(formatter);
    }
}
